Micron Document
C.S.Burner 🪙 ━►🔥GIT Node


Displaying Raw • Download

meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue 65ca9ddc823fcc2c8f8653c553b944b85db0875f (65ca9ddc) Text, 87.72 KB

T8b949e<!-- SPDX-License-Identifier: 0BSD AND MIT -->

Tff7b72<templateTff7b72>
Tff7b72<div Te6edf3:class=Ta5d6ff"Ta5d6ffsidebarRootClassTa5d6ff"Tff7b72>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffeffectiveCollapsedTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col h-full min-h-0 bg-white dark:bg-zinc-950 border-r border-gray-200 dark:border-zinc-800Ta5d6ff"
Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffhidden sm:flex h-10 shrink-0 items-center justify-end border-b border-gray-200 dark:border-zinc-800 px-2Ta5d6ff"
Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffp-1.5 rounded-lg text-gray-500 hover:bg-gray-100 dark:text-zinc-400 dark:hover:bg-zinc-800 transition-colorsTa5d6ff"
@Te6edf3click=Ta5d6ff"Te6edf3$emitTa5d6ff(Ta5d6ff'Ta5d6fftoggle-collapseTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffchevron-rightTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-5Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col items-center gap-1 py-2 px-1 border-b border-gray-200 dark:border-zinc-800Ta5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffp-2 rounded-xl transition-colorsTa5d6ff"
Te6edf3:class=Ta5d6ff"
Ta5d6ff tab === Ta5d6ff'Ta5d6fffavouritesTa5d6ff'
Ta5d6ff ? Ta5d6ff'Ta5d6ffbg-blue-600 text-white dark:bg-blue-500Ta5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6fftext-gray-500 hover:bg-gray-100 dark:text-zinc-400 dark:hover:bg-zinc-800Ta5d6ff'
Ta5d6ff Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6fftab = Ta5d6ff'Ta5d6fffavouritesTa5d6ff'Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffstarTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-6Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffp-2 rounded-xl transition-colorsTa5d6ff"
Te6edf3:class=Ta5d6ff"
Ta5d6ff tab === Ta5d6ff'Ta5d6ffannouncesTa5d6ff'
Ta5d6ff ? Ta5d6ff'Ta5d6ffbg-blue-600 text-white dark:bg-blue-500Ta5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6fftext-gray-500 hover:bg-gray-100 dark:text-zinc-400 dark:hover:bg-zinc-800Ta5d6ff'
Ta5d6ff Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6fftab = Ta5d6ff'Ta5d6ffannouncesTa5d6ff'Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffsatellite-uplinkTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-6Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6fftab === Ta5d6ff'Ta5d6fffavouritesTa5d6ff'Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 min-h-0 w-full overflow-y-auto overflow-x-hidden flex flex-col items-center gap-1 py-1 px-0.5Ta5d6ff"
Tff7b72>
Tff7b72<button
Te6edf3v-for=Ta5d6ff"Ta5d6fffav in collapsedFavouritePreviewTa5d6ff"
Te6edf3:key=Ta5d6ff"Ta5d6fffav.destination_hashTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffshrink-0 p-1 rounded-xl transition-colors focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500Ta5d6ff"
Te6edf3:class=Ta5d6ff"
Ta5d6ff fav.destination_hash === selectedDestinationHash
? Ta5d6ff'Ta5d6ffring-2 ring-blue-500 ring-offset-1 ring-offset-white dark:ring-offset-zinc-950Ta5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6ffhover:bg-white/10Ta5d6ff'
Ta5d6ff Ta5d6ff"
Te6edf3:title=Ta5d6ff"Ta5d6fffavouriteDisplayName(fav)Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffonFavouriteClick(fav)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffserver-networkTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-6 text-gray-600 dark:text-gray-300Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</div>
Tff7b72<div
Te6edf3v-else-if=Ta5d6ff"Ta5d6fftab === Ta5d6ff'Ta5d6ffannouncesTa5d6ff'Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 min-h-0 w-full overflow-y-auto overflow-x-hidden flex flex-col items-center gap-1 py-1 px-0.5Ta5d6ff"
Tff7b72>
Tff7b72<button
Te6edf3v-for=Ta5d6ff"Ta5d6ffnode in collapsedAnnounceNodesPreviewTa5d6ff"
Te6edf3:key=Ta5d6ff"Ta5d6ffnode.destination_hashTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffshrink-0 p-1 rounded-xl transition-colors focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500Ta5d6ff"
Te6edf3:class=Ta5d6ff"
Ta5d6ff node.destination_hash === selectedDestinationHash
? Ta5d6ff'Ta5d6ffring-2 ring-blue-500 ring-offset-1 ring-offset-white dark:ring-offset-zinc-950Ta5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6ffhover:bg-white/10Ta5d6ff'
Ta5d6ff Ta5d6ff"
Te6edf3:title=Ta5d6ff"Ta5d6ffnode.custom_display_name || node.display_nameTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffonNodeClick(node)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffsatellite-uplinkTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-6 text-gray-600 dark:text-gray-300Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72<template v-elseTff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ff-mb-px flex h-10 min-w-0 items-stretch border-b border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950Ta5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex min-w-0 flex-1Ta5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffsidebar-tabTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff{ Ta5d6ff'Ta5d6ffsidebar-tab--activeTa5d6ff'Ta5d6ff: tab === Ta5d6ff'Ta5d6fffavouritesTa5d6ff'Ta5d6ff }Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6fftab = Ta5d6ff'Ta5d6fffavouritesTa5d6ff'Ta5d6ff"
Tff7b72>
{{ Te6edf3$t("nomadnet.favourites") }}
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffsidebar-tabTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff{ Ta5d6ff'Ta5d6ffsidebar-tab--activeTa5d6ff'Ta5d6ff: tab === Ta5d6ff'Ta5d6ffannouncesTa5d6ff'Ta5d6ff }Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6fftab = Ta5d6ff'Ta5d6ffannouncesTa5d6ff'Ta5d6ff"
Tff7b72>
{{ Te6edf3$t("nomadnet.announces") }}
Tff7b72</button>
Tff7b72</div>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffhidden sm:flex shrink-0 items-center border-b-2 border-transparent px-1.5 text-gray-500 hover:bg-gray-100 dark:text-zinc-400 dark:hover:bg-zinc-800 transition-colorsTa5d6ff"
@Te6edf3click=Ta5d6ff"Te6edf3$emitTa5d6ff(Ta5d6ff'Ta5d6fftoggle-collapseTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffchevron-leftTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-5Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</div>

Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6fftab === Ta5d6ff'Ta5d6fffavouritesTa5d6ff'Ta5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 flex flex-col min-h-0Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffp-3 border-b border-gray-200 dark:border-zinc-800 space-y-2Ta5d6ff"Tff7b72>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6fffavouritesSearchTermTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3:placeholder=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffnomadnet.search_favourites_placeholderTa5d6ff'Ta5d6ff, { count: favourites.length })Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-field w-full rounded-noneTa5d6ff"
Tff7b72/>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6fffavouritesSelectionModeTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col gap-2 px-2 py-2 bg-blue-50 dark:bg-blue-900/10 rounded-lgTa5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 min-w-0 w-fullTa5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 min-w-0 flex-1 overflow-hiddenTa5d6ff"Tff7b72>
Tff7b72<input
Te6edf3type=Ta5d6ff"Ta5d6ffcheckboxTa5d6ff"
Te6edf3:checked=Ta5d6ff"Ta5d6ffallVisibleFavouritesSelectedTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffrounded-sm border-gray-300 text-blue-600 focus:ring-blue-500 shrink-0Ta5d6ff"
@Te6edf3change=Ta5d6ff"Ta5d6fftoggleSelectAllVisibleFavouritesTa5d6ff"
Tff7b72/>
Tff7b72<span
Te6edf3class=Ta5d6ff"Ta5d6fftext-xs font-semibold text-blue-700 dark:text-blue-400 truncate leading-noneTa5d6ff"
Tff7b72>
{{ Te6edf3$t("nomadnet.bulk_selected_count", { count: selectedFavouriteHashes.length }) }}
Tff7b72</span>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 shrink-0Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffrelative inline-flex items-centerTa5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinline-flex items-center whitespace-nowrap rounded px-0 py-0.5 text-xs font-bold leading-none text-blue-600 dark:text-blue-400 hover:underline disabled:pointer-events-none disabled:opacity-40Ta5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffselectedFavouriteHashes.length === 0Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6fffavouriteBulkMoveMenuOpen = !favouriteBulkMoveMenuOpenTa5d6ff"
Tff7b72>
{{ Te6edf3$t("nomadnet.bulk_move_to_section") }}
Tff7b72</button>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6fffavouriteBulkMoveMenuOpenTa5d6ff"
Te6edf3v-click-outside=Ta5d6ff"Ta5d6ff{ handler: closeFavouriteBulkMoveMenu, capture: true }Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffabsolute right-0 top-full mt-1 z-60 min-w-[10rem] max-h-56 overflow-y-auto bg-white dark:bg-zinc-800 rounded-xl shadow-xl border border-gray-200 dark:border-zinc-700 py-1Ta5d6ff"
Tff7b72>
Tff7b72<button
Te6edf3v-for=Ta5d6ff"Ta5d6ffsection in orderedSectionsTa5d6ff"
Te6edf3:key=Ta5d6ff"Ta5d6ff'Ta5d6ffbulk-move-Ta5d6ff'Ta5d6ff + section.idTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full text-left px-3 py-2 text-sm text-gray-700 dark:text-zinc-300 hover:bg-gray-100 dark:hover:bg-zinc-700Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffbulkMoveSelectedFavouritesToSection(section.id)Ta5d6ff"
Tff7b72>
{{ section.name }}
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinline-flex items-center whitespace-nowrap rounded px-0 py-0.5 text-xs font-bold leading-none text-red-600 dark:text-red-400 hover:underline disabled:pointer-events-none disabled:opacity-40Ta5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffselectedFavouriteHashes.length === 0Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffbulkRemoveSelectedFavouritesTa5d6ff"
Tff7b72>
{{ Te6edf3$t("nomadnet.bulk_remove") }}
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center justify-between px-3 pt-2 text-[11px] uppercase tracking-wide text-gray-500 dark:text-gray-400Ta5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-1 min-w-0Ta5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffshrink-0 inline-flex items-center justify-center p-0.5 rounded-sm text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 transition-colors leading-noneTa5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffnomadnet.sidebar_selection_modeTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff{ Ta5d6ff'Ta5d6fftext-blue-500 dark:text-blue-400Ta5d6ff'Ta5d6ff: favouritesSelectionMode }Ta5d6ff"
@click.Te6edf3stop=Ta5d6ff"Ta5d6fftoggleFavouritesSelectionModeTa5d6ff"
Tff7b72>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6ffblock size-[14px]Ta5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffcheckbox-multiple-marked-outlineTa5d6ff" Tff7b72/>
Tff7b72</span>
Tff7b72</button>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fffont-semibold truncateTa5d6ff"Tff7b72>SectionsTff7b72</span>
Tff7b72</div>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinline-flex items-center gap-1 text-xs font-semibold text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffcreateSectionTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffplusTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4Ta5d6ff" Tff7b72/>
Tff7b72<spanTff7b72>Add SectionTff7b72</span>
Tff7b72</button>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 overflow-y-auto px-2 pb-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6fffavouritesSearchNoResultsTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffempty-state empty-state--panelTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffstar-outlineTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-8 h-8Ta5d6ff" Tff7b72/>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fffont-semiboldTa5d6ff"Tff7b72>{{ Te6edf3$t("nomadnet.favourites_search_no_results") }}Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-sm text-gray-500 dark:text-gray-400Ta5d6ff"Tff7b72>
{{ Te6edf3$t("nomadnet.favourites_search_try_other") }}
Tff7b72</div>
Tff7b72</div>
Tff7b72<div v-else Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-3 pt-2Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6fffavourites.length === 0Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffempty-state empty-state--compact border border-dashed border-gray-200 dark:border-zinc-800 rounded-xl py-4 mb-1Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffstar-outlineTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-8 h-8Ta5d6ff" Tff7b72/>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fffont-semiboldTa5d6ff"Tff7b72>{{ Te6edf3$t("nomadnet.no_favourites") }}Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-sm text-gray-500 dark:text-gray-400Ta5d6ff"Tff7b72>
{{ Te6edf3$t("nomadnet.add_nodes_from_announces") }}
Tff7b72</div>
Tff7b72</div>
Tff7b72<div
Te6edf3v-for=Ta5d6ff"Ta5d6ffsection in sectionsWithFavouritesTa5d6ff"
Te6edf3:key=Ta5d6ff"Ta5d6ffsection.idTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffrounded-xlTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff[
dragOverSectionId === section.id
? Ta5d6ff'Ta5d6ffring-1 ring-blue-400 dark:ring-blue-600 bg-blue-50/40 dark:bg-blue-900/10Ta5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6ff'Ta5d6ff,
draggingSectionOverId === section.id
? Ta5d6ff'Ta5d6ffring-1 ring-blue-300 dark:ring-blue-700 bg-blue-50/30 dark:bg-blue-900/5Ta5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6ff'Ta5d6ff,
]Ta5d6ff"
@dragover.Te6edf3prevent=Ta5d6ff"Ta5d6ffonSectionDragOver(section.id)Ta5d6ff"
@Te6edf3dragleave=Ta5d6ff"Ta5d6ffonSectionDragLeaveTa5d6ff"
@drop.Te6edf3prevent=Ta5d6ff"Ta5d6ffonDropOnSection(section.id)Ta5d6ff"
Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center justify-between px-2 py-1 cursor-pointer select-noneTa5d6ff"
Te6edf3draggable=Ta5d6ff"Ta5d6fftrueTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6fftoggleSectionCollapse(section.id)Ta5d6ff"
@contextmenu.Te6edf3prevent=Ta5d6ff"Ta5d6ffopenSectionContextMenu(Te6edf3$eventTa5d6ff, section)Ta5d6ff"
@Te6edf3dragstart=Ta5d6ff"Ta5d6ffonSectionDragStart(section.id)Ta5d6ff"
@dragover.Te6edf3prevent=Ta5d6ff"Ta5d6ffonSectionReorderDragOver(section.id)Ta5d6ff"
@drop.Te6edf3prevent=Ta5d6ff"Ta5d6ffonSectionDrop(section.id)Ta5d6ff"
@Te6edf3dragend=Ta5d6ff"Ta5d6ffonSectionDragEndTa5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 flex-1 min-w-0Ta5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3:icon-name=Ta5d6ff"Ta5d6ffsection.collapsed ? Ta5d6ff'Ta5d6ffchevron-rightTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6ffchevron-downTa5d6ff'Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 text-gray-400 shrink-0Ta5d6ff"
Tff7b72/>
Tff7b72<template Te6edf3v-if=Ta5d6ff"Ta5d6ffeditingSectionId === section.idTa5d6ff"Tff7b72>
Tff7b72<input
Te6edf3:ref=Ta5d6ff"Ta5d6ff`sectionInput-Tff7b72${Te6edf3sectionTff7b72.Td2a8ffidTff7b72}Ta5d6ff`Ta5d6ff"
Te6edf3v-model=Ta5d6ff"Ta5d6ffeditingSectionNameTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 bg-transparent border-b border-blue-500 text-xs font-semibold uppercase tracking-wide text-gray-900 dark:text-white focus:outline-hidden min-w-0Ta5d6ff"
@click.stop
@keydown.Te6edf3enter=Ta5d6ff"Ta5d6ffsaveSectionNameTa5d6ff"
@keydown.Te6edf3esc=Ta5d6ff"Ta5d6ffcancelEditingSectionTa5d6ff"
@Te6edf3blur=Ta5d6ff"Ta5d6ffsaveSectionNameTa5d6ff"
Tff7b72/>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffp-1 text-green-500 hover:text-green-600 shrink-0Ta5d6ff"
@click.Te6edf3stop=Ta5d6ff"Ta5d6ffsaveSectionNameTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffcheckTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</template>
Tff7b72<span
v-else
Te6edf3class=Ta5d6ff"Ta5d6fftext-xs font-semibold uppercase tracking-wide text-gray-600 dark:text-gray-300 truncateTa5d6ff"
@click.Te6edf3stop=Ta5d6ff"Ta5d6ffstartEditingSection(section)Ta5d6ff"
Tff7b72>
{{ section.name }}
Tff7b72</span>
Tff7b72<span
Te6edf3v-if=Ta5d6ff"Ta5d6ffsection.collapsedTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fftext-[10px] font-semibold text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-zinc-800 px-2 py-0.5 rounded-full shrink-0Ta5d6ff"
Tff7b72>
{{ section.favourites.length }}
Tff7b72</span>
Tff7b72</div>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffh-px bg-gray-200 dark:bg-zinc-800 mx-1Ta5d6ff"Tff7b72>Tff7b72</div>
Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ff!section.collapsedTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-2 pt-2 pb-1 px-1Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3v-for=Ta5d6ff"Ta5d6fffavourite of section.favouritesTa5d6ff"
Te6edf3:key=Ta5d6ff"Ta5d6fffavourite.destination_hashTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fffavourite-card relativeTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff[
favourite.destination_hash === selectedDestinationHash
? Ta5d6ff'Ta5d6fffavourite-card--activeTa5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6ff'Ta5d6ff,
isFavouriteRowDragging(favourite.destination_hash)
? Ta5d6ff'Ta5d6fffavourite-card--draggingTa5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6ff'Ta5d6ff,
favouritesSelectionMode &&
selectedFavouriteHashes.includes(favourite.destination_hash)
? Ta5d6ff'Ta5d6ffring-1 ring-blue-400/60 dark:ring-blue-500/50Ta5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6ff'Ta5d6ff,
]Ta5d6ff"
Te6edf3draggable=Ta5d6ff"Ta5d6fftrueTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffonFavouriteRowActivate(favourite)Ta5d6ff"
@contextmenu.Te6edf3prevent=Ta5d6ff"Ta5d6ffopenFavouriteContextMenu(Te6edf3$eventTa5d6ff, favourite, section.id)Ta5d6ff"
@Te6edf3dragstart=Ta5d6ff"Ta5d6ffonFavouriteDragStart(Te6edf3$eventTa5d6ff, favourite, section.id)Ta5d6ff"
@dragover.Te6edf3prevent=Ta5d6ff"Ta5d6ffonFavouriteDragOver(Te6edf3$eventTa5d6ff)Ta5d6ff"
@drop.Te6edf3prevent=Ta5d6ff"Ta5d6ffonFavouriteDrop(Te6edf3$eventTa5d6ff, section.id, favourite)Ta5d6ff"
@Te6edf3dragend=Ta5d6ff"Ta5d6ffonFavouriteDragEndTa5d6ff"
Tff7b72>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6fffavouritesSelectionModeTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffmy-auto mr-1 px-0.5 shrink-0Ta5d6ff"
@click.stop
Tff7b72>
Tff7b72<input
Te6edf3type=Ta5d6ff"Ta5d6ffcheckboxTa5d6ff"
Te6edf3:checked=Ta5d6ff"Ta5d6ffselectedFavouriteHashes.includes(favourite.destination_hash)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffrounded-sm border-gray-300 text-blue-600 focus:ring-blue-500Ta5d6ff"
@Te6edf3change=Ta5d6ff"Ta5d6fftoggleSelectFavourite(favourite.destination_hash)Ta5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"
Ta5d6ff GlobalState.config.banished_effect_enabled &&
isBlocked(favourite.destination_hash)
Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffbanished-overlayTa5d6ff"
Te6edf3:style=Ta5d6ff"Ta5d6ff{ background: GlobalState.config.banished_color + Ta5d6ff'Ta5d6ff33Ta5d6ff'Ta5d6ff }Ta5d6ff"
Tff7b72>
Tff7b72<span
Te6edf3class=Ta5d6ff"Ta5d6ffbanished-text text-[10px]! opacity-100! tracking-widest! border! px-1! py-0.5! text-white! shadow-lg!Ta5d6ff"
Te6edf3:style=Ta5d6ff"Ta5d6ff{ Ta5d6ff'Ta5d6ffbackground-colorTa5d6ff'Ta5d6ff: GlobalState.config.banished_color }Ta5d6ff"
Tff7b72>{{ GlobalState.config.banished_text }}Tff7b72</span
>
Tff7b72</div>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fffavourite-card__icon shrink-0Ta5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffserver-networkTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-5 h-5Ta5d6ff" Tff7b72/>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmin-w-0 flex-1Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6fftext-sm font-semibold text-gray-900 dark:text-white truncateTa5d6ff"
Te6edf3:title=Ta5d6ff"Ta5d6fffavouriteDisplayName(favourite)Ta5d6ff"
Tff7b72>
{{ favouriteDisplayName(favourite) }}
Tff7b72</div>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6fftext-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 cursor-pointer inline-flex items-centerTa5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffcommon.copy_to_clipboardTa5d6ff'Ta5d6ff)Ta5d6ff"
@click.Te6edf3stop=Ta5d6ff"Ta5d6ffcopyToClipboard(favourite.destination_hash, Ta5d6ff'Ta5d6ffAddressTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
{{ formatDestinationHash(favourite.destination_hash) }}
Tff7b72</div>
Tff7b72</div>
Tff7b72<IconButton
Te6edf3class=Ta5d6ff"Ta5d6ffshrink-0 text-gray-500 dark:text-gray-300Ta5d6ff"
@click.Te6edf3stop=Ta5d6ff"Ta5d6ffopenFavouriteContextMenu(Te6edf3$eventTa5d6ff, favourite, section.id)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffdots-verticalTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-5 h-5Ta5d6ff" Tff7b72/>
Tff7b72</IconButton>
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffsection.favourites.length === 0Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fftext-xs text-gray-500 dark:text-gray-400 px-3 pb-2 italicTa5d6ff"
Tff7b72>
{{ Te6edf3$t("nomadnet.no_favourites_in_section") }}
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

T8b949e<!-- Favourite Context Menu (Teleport to body to avoid overflow clipping) -->
Tff7b72<Teleport Te6edf3to=Ta5d6ff"Ta5d6ffbodyTa5d6ff"Tff7b72>
Tff7b72<ContextMenuPanel
Te6edf3v-click-outside=Ta5d6ff"Ta5d6ff{
handler: () => {
if (!favouriteContextMenu.justOpened) closeContextMenus();
},
capture: true,
}Ta5d6ff"
Te6edf3:show=Ta5d6ff"Ta5d6fffavouriteContextMenu.showTa5d6ff"
Te6edf3:x=Ta5d6ff"Ta5d6fffavouriteContextMenu.xTa5d6ff"
Te6edf3:y=Ta5d6ff"Ta5d6fffavouriteContextMenu.yTa5d6ff"
Te6edf3panel-class=Ta5d6ff"Ta5d6ffz-200 min-w-56Ta5d6ff"
Tff7b72>
Tff7b72<ContextMenuItem @Te6edf3click=Ta5d6ff"Ta5d6ffrenameFavouriteFromContextTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffpencilTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 text-gray-400Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("nomadnet.rename") }}
Tff7b72</ContextMenuItem>
Tff7b72<ContextMenuItem
Te6edf3v-if=Ta5d6ff"Ta5d6ff!isBlocked(favouriteContextMenu.targetHash)Ta5d6ff"
Te6edf3item-class=Ta5d6ff"Ta5d6fftext-red-600 dark:text-red-400Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffbanishFavouriteFromContextTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffgavelTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 text-red-400Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("nomadnet.block_node") }}
Tff7b72</ContextMenuItem>
Tff7b72<ContextMenuItem
v-else
Te6edf3item-class=Ta5d6ff"Ta5d6fftext-emerald-600 dark:text-emerald-400Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffunblockFavouriteFromContextTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffcheck-circleTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 text-emerald-500Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("nomadnet.lift_banishment") }}
Tff7b72</ContextMenuItem>
Tff7b72<ContextMenuItem
Te6edf3item-class=Ta5d6ff"Ta5d6fftext-red-600 dark:text-red-400Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffremoveFavouriteFromContextTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6fftrash-canTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 text-red-400Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("nomadnet.remove") }}
Tff7b72</ContextMenuItem>
Tff7b72<ContextMenuDivider Tff7b72/>
Tff7b72<ContextMenuSectionLabelTff7b72>Move to SectionTff7b72</ContextMenuSectionLabel>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmax-h-56 overflow-y-auto custom-scrollbarTa5d6ff"Tff7b72>
Tff7b72<ContextMenuItem
Te6edf3v-for=Ta5d6ff"Ta5d6ffsection in sectionsWithFavouritesTa5d6ff"
Te6edf3:key=Ta5d6ff"Ta5d6ffsection.id + Ta5d6ff'Ta5d6ff-moveTa5d6ff'Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffmoveContextFavouriteToSection(section.id)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6fffolderTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 opacity-70Ta5d6ff" Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftruncateTa5d6ff"Tff7b72>{{ section.name }}Tff7b72</span>
Tff7b72</ContextMenuItem>
Tff7b72</div>
Tff7b72</ContextMenuPanel>
Tff7b72</Teleport>

T8b949e<!-- Section Context Menu (Teleport to body) -->
Tff7b72<Teleport Te6edf3to=Ta5d6ff"Ta5d6ffbodyTa5d6ff"Tff7b72>
Tff7b72<ContextMenuPanel
Te6edf3v-click-outside=Ta5d6ff"Ta5d6ff{ handler: closeContextMenus, capture: true }Ta5d6ff"
Te6edf3:show=Ta5d6ff"Ta5d6ffsectionContextMenu.showTa5d6ff"
Te6edf3:x=Ta5d6ff"Ta5d6ffsectionContextMenu.xTa5d6ff"
Te6edf3:y=Ta5d6ff"Ta5d6ffsectionContextMenu.yTa5d6ff"
Te6edf3panel-class=Ta5d6ff"Ta5d6ffz-200Ta5d6ff"
Tff7b72>
Tff7b72<ContextMenuItem @Te6edf3click=Ta5d6ff"Ta5d6ffrenameSectionFromContextTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffpencilTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 text-gray-400Ta5d6ff" Tff7b72/>
Rename Section
Tff7b72</ContextMenuItem>
Tff7b72<ContextMenuItem @Te6edf3click=Ta5d6ff"Ta5d6ffexportSectionFavouritesFromContextTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6fffile-exportTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 text-gray-400Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("nomadnet.export_section_favourites") }}
Tff7b72</ContextMenuItem>
Tff7b72<ContextMenuItem
Te6edf3:item-class=Ta5d6ff"
Ta5d6ff Ta5d6ff'Ta5d6fftext-red-600 dark:text-red-400Ta5d6ff'Ta5d6ff +
(sectionContextMenu.sectionId === defaultSectionId
? Ta5d6ff'Ta5d6ff opacity-50 cursor-not-allowedTa5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6ff'Ta5d6ff)
Ta5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffsectionContextMenu.sectionId === defaultSectionIdTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffremoveSectionFromContextTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffdeleteTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 text-red-400Ta5d6ff" Tff7b72/>
Delete Section
Tff7b72</ContextMenuItem>
Tff7b72</ContextMenuPanel>
Tff7b72</Teleport>
Tff7b72</div>

Tff7b72<div v-else Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 flex flex-col min-h-0Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffp-3 border-b border-gray-200 dark:border-zinc-800 space-y-2Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex gap-1.5 items-centerTa5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffrelative flex-1 min-w-0Ta5d6ff"Tff7b72>
Tff7b72<input
Te6edf3:value=Ta5d6ff"Ta5d6ffnodesSearchTermTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3:placeholder=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffnomadnet.search_placeholder_announcesTa5d6ff'Ta5d6ff, { count: totalNodesCount })Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-field w-full min-w-0 rounded-noneTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff{ Ta5d6ff'Ta5d6ffpr-7Ta5d6ff'Ta5d6ff: isSearchingNodes }Ta5d6ff"
@Te6edf3input=Ta5d6ff"Ta5d6ffonNodesSearchInputTa5d6ff"
Tff7b72/>
Tff7b72<span
Te6edf3v-if=Ta5d6ff"Ta5d6ffisSearchingNodesTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffabsolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-gray-400Ta5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffnomadnet.searching_announcesTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffloadingTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 animate-spinTa5d6ff" Tff7b72/>
Tff7b72</span>
Tff7b72</div>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffshrink-0 self-center inline-flex items-center justify-center p-0.5 rounded-sm text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 transition-colors leading-noneTa5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffnomadnet.sidebar_selection_modeTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff{ Ta5d6ff'Ta5d6fftext-blue-500 dark:text-blue-400Ta5d6ff'Ta5d6ff: announcesSelectionMode }Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6fftoggleAnnouncesSelectionModeTa5d6ff"
Tff7b72>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6ffblock size-[14px]Ta5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffcheckbox-multiple-marked-outlineTa5d6ff" Tff7b72/>
Tff7b72</span>
Tff7b72</button>
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffannouncesSelectionModeTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col gap-2 px-2 py-2 bg-blue-50 dark:bg-blue-900/10 rounded-lgTa5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 min-w-0 w-fullTa5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 min-w-0 flex-1 overflow-hiddenTa5d6ff"Tff7b72>
Tff7b72<input
Te6edf3type=Ta5d6ff"Ta5d6ffcheckboxTa5d6ff"
Te6edf3:checked=Ta5d6ff"Ta5d6ffallVisibleAnnouncesSelectedTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffrounded-sm border-gray-300 text-blue-600 focus:ring-blue-500 shrink-0Ta5d6ff"
@Te6edf3change=Ta5d6ff"Ta5d6fftoggleSelectAllVisibleAnnouncesTa5d6ff"
Tff7b72/>
Tff7b72<span
Te6edf3class=Ta5d6ff"Ta5d6fftext-xs font-semibold text-blue-700 dark:text-blue-400 truncate leading-noneTa5d6ff"
Tff7b72>
{{ Te6edf3$t("nomadnet.bulk_selected_count", { count: selectedAnnounceHashes.length }) }}
Tff7b72</span>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2 shrink-0Ta5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinline-flex items-center whitespace-nowrap rounded px-0 py-0.5 text-xs font-bold leading-none text-yellow-600 dark:text-yellow-400 hover:underline disabled:pointer-events-none disabled:opacity-40Ta5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffselectedAnnounceHashes.length === 0Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffbulkAddSelectedAnnouncesToFavouritesTa5d6ff"
Tff7b72>
{{ Te6edf3$t("nomadnet.bulk_add_to_favourites") }}
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinline-flex items-center whitespace-nowrap rounded px-0 py-0.5 text-xs font-bold leading-none text-red-600 dark:text-red-400 hover:underline disabled:pointer-events-none disabled:opacity-40Ta5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffselectedAnnounceHashes.length === 0Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffbulkBanishSelectedAnnouncesTa5d6ff"
Tff7b72>
{{ Te6edf3$t("nomadnet.bulk_block_nodes") }}
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 overflow-y-auto px-2 pb-4Ta5d6ff" @Te6edf3scroll=Ta5d6ff"Ta5d6ffonNodesScrollTa5d6ff"Tff7b72>
Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ffsearchedNodes.length > 0Ta5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-2 pt-2Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3v-for=Ta5d6ff"Ta5d6ffnode of searchedNodesTa5d6ff"
Te6edf3:key=Ta5d6ff"Ta5d6ffnode.destination_hashTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffannounce-card relativeTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff[
node.destination_hash === selectedDestinationHash ? Ta5d6ff'Ta5d6ffannounce-card--activeTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6ff'Ta5d6ff,
announcesSelectionMode && selectedAnnounceHashes.includes(node.destination_hash)
? Ta5d6ff'Ta5d6ffring-1 ring-blue-400/60 dark:ring-blue-500/50Ta5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6ff'Ta5d6ff,
]Ta5d6ff"
@contextmenu.Te6edf3prevent=Ta5d6ff"Ta5d6ffopenAnnounceContextMenu(Te6edf3$eventTa5d6ff, node)Ta5d6ff"
Tff7b72>
T8b949e<!-- banished overlay -->
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffGlobalState.config.banished_effect_enabled && isBlocked(node.identity_hash)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffbanished-overlayTa5d6ff"
Te6edf3:style=Ta5d6ff"Ta5d6ff{ background: GlobalState.config.banished_color + Ta5d6ff'Ta5d6ff33Ta5d6ff'Ta5d6ff }Ta5d6ff"
Tff7b72>
Tff7b72<span
Te6edf3class=Ta5d6ff"Ta5d6ffbanished-text text-[10px]! opacity-100! tracking-widest! border! px-1! py-0.5! text-white! shadow-lg!Ta5d6ff"
Te6edf3:style=Ta5d6ff"Ta5d6ff{ Ta5d6ff'Ta5d6ffbackground-colorTa5d6ff'Ta5d6ff: GlobalState.config.banished_color }Ta5d6ff"
Tff7b72>{{ GlobalState.config.banished_text }}Tff7b72</span
>
Tff7b72</div>

Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-3 flex-1 min-w-0 cursor-pointerTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffonAnnounceRowActivate(node)Ta5d6ff"
Tff7b72>
Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ffannouncesSelectionModeTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffmy-auto shrink-0 px-0.5Ta5d6ff" @click.stopTff7b72>
Tff7b72<input
Te6edf3type=Ta5d6ff"Ta5d6ffcheckboxTa5d6ff"
Te6edf3:checked=Ta5d6ff"Ta5d6ffselectedAnnounceHashes.includes(node.destination_hash)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffrounded-sm border-gray-300 text-blue-600 focus:ring-blue-500Ta5d6ff"
@Te6edf3change=Ta5d6ff"Ta5d6fftoggleSelectAnnounce(node.destination_hash)Ta5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffannounce-card__icon shrink-0Ta5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffsatellite-uplinkTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-5 h-5Ta5d6ff" Tff7b72/>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmin-w-0 flex-1Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6fftext-sm font-semibold text-gray-900 dark:text-white truncateTa5d6ff"
Te6edf3:title=Ta5d6ff"Ta5d6ffnode.custom_display_name || node.display_nameTa5d6ff"
Tff7b72>
{{ node.custom_display_name || node.display_name }}
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs text-gray-500 dark:text-gray-400 flex flex-col gap-0.5Ta5d6ff"Tff7b72>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftruncateTa5d6ff"Tff7b72>{{
Te6edf3$t("nomadnet.announced_time_ago", {
time: formatTimeAgoForI18n(node.updated_at),
})
}}Tff7b72</span>
Tff7b72<span
Te6edf3class=Ta5d6ff"Ta5d6ffcursor-pointer hover:text-blue-500 dark:hover:text-blue-400 inline-flex items-centerTa5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffcommon.copy_to_clipboardTa5d6ff'Ta5d6ff)Ta5d6ff"
@click.Te6edf3stop=Ta5d6ff"Ta5d6ffcopyToClipboard(node.destination_hash, Ta5d6ff'Ta5d6ffAddressTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
{{ formatDestinationHash(node.destination_hash) }}
Tff7b72</span>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffshrink-0Ta5d6ff"Tff7b72>
Tff7b72<DropDownMenuTff7b72>
Tff7b72<template #buttonTff7b72>
Tff7b72<IconButtonTff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffdots-verticalTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-5 h-5Ta5d6ff" Tff7b72/>
Tff7b72</IconButton>
Tff7b72</template>
Tff7b72<template #itemsTff7b72>
Tff7b72<DropDownMenuItem
Te6edf3v-if=Ta5d6ff"Ta5d6ff!isBlocked(node.identity_hash)Ta5d6ff"
@click.Te6edf3stop=Ta5d6ff"Ta5d6ffonBlockNode(node)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffgavelTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-5 h-5 text-red-500Ta5d6ff" Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftext-red-500Ta5d6ff"Tff7b72>{{ Te6edf3$t("nomadnet.block_node") }}Tff7b72</span>
Tff7b72</DropDownMenuItem>
Tff7b72<DropDownMenuItem v-else @click.Te6edf3stop=Ta5d6ff"Ta5d6ffonUnblockNode(node.identity_hash)Ta5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3icon-name=Ta5d6ff"Ta5d6ffcheck-circleTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-5 h-5 text-green-500Ta5d6ff"
Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftext-green-500Ta5d6ff"Tff7b72>{{ Te6edf3$t("nomadnet.lift_banishment") }}Tff7b72</span>
Tff7b72</DropDownMenuItem>
Tff7b72</template>
Tff7b72</DropDownMenu>
Tff7b72</div>
Tff7b72</div>

T8b949e<!-- loading more spinner -->
Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ffisLoadingMoreNodesTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffp-4 text-centerTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffloadingTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-6 animate-spin text-gray-400Ta5d6ff" Tff7b72/>
Tff7b72</div>
Tff7b72</div>
Tff7b72<div
Te6edf3v-else-if=Ta5d6ff"Ta5d6ffisSearchingNodes && nodesSearchTerm.trim() !== Ta5d6ff'Ta5d6ff'Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffempty-state empty-state--panelTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffloadingTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-8 h-8 animate-spinTa5d6ff" Tff7b72/>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fffont-semiboldTa5d6ff"Tff7b72>{{ Te6edf3$t("nomadnet.searching_announces") }}Tff7b72</div>
Tff7b72</div>
Tff7b72<div Te6edf3v-else-if=Ta5d6ff"Ta5d6ffnodesSearchTerm.trim() !== Ta5d6ff'Ta5d6ff'Ta5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffempty-state empty-state--panelTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffradarTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-8 h-8Ta5d6ff" Tff7b72/>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fffont-semiboldTa5d6ff"Tff7b72>{{ Te6edf3$t("nomadnet.no_search_results_peers") }}Tff7b72</div>
Tff7b72</div>
Tff7b72<div v-else Te6edf3class=Ta5d6ff"Ta5d6ffempty-state empty-state--panelTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffradarTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-8 h-8Ta5d6ff" Tff7b72/>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fffont-semiboldTa5d6ff"Tff7b72>{{ Te6edf3$t("nomadnet.no_announces_yet") }}Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-sm text-gray-500 dark:text-gray-400Ta5d6ff"Tff7b72>
{{ Te6edf3$t("nomadnet.listening_for_peers") }}
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

T8b949e<!-- Announce Context Menu (right-click, Teleport to body) -->
Tff7b72<Teleport Te6edf3to=Ta5d6ff"Ta5d6ffbodyTa5d6ff"Tff7b72>
Tff7b72<ContextMenuPanel
Te6edf3v-click-outside=Ta5d6ff"Ta5d6ff{
handler: () => {
if (!announceContextMenu.justOpened) closeContextMenus();
},
capture: true,
}Ta5d6ff"
Te6edf3:show=Ta5d6ff"Ta5d6ffannounceContextMenu.showTa5d6ff"
Te6edf3:x=Ta5d6ff"Ta5d6ffannounceContextMenu.xTa5d6ff"
Te6edf3:y=Ta5d6ff"Ta5d6ffannounceContextMenu.yTa5d6ff"
Te6edf3panel-class=Ta5d6ff"Ta5d6ffz-200Ta5d6ff"
Tff7b72>
Tff7b72<ContextMenuItem
Te6edf3v-if=Ta5d6ff"Ta5d6ff!isFavourite(announceContextMenu.node?.destination_hash)Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffaddFavouriteFromContextTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffstar-outlineTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 text-yellow-500Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("nomadnet.add_favourite") }}
Tff7b72</ContextMenuItem>
Tff7b72<ContextMenuItem
Te6edf3v-if=Ta5d6ff"Ta5d6ff!isBlocked(announceContextMenu.node?.identity_hash)Ta5d6ff"
Te6edf3item-class=Ta5d6ff"Ta5d6fftext-red-600 dark:text-red-400Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffblockAnnounceFromContextTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffgavelTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 text-red-400Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("nomadnet.block_node") }}
Tff7b72</ContextMenuItem>
Tff7b72<ContextMenuItem
v-else
Te6edf3item-class=Ta5d6ff"Ta5d6fftext-emerald-600 dark:text-emerald-400Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffunblockAnnounceFromContextTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffcheck-circleTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 text-emerald-500Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("nomadnet.lift_banishment") }}
Tff7b72</ContextMenuItem>
Tff7b72</ContextMenuPanel>
Tff7b72</Teleport>
Tff7b72</div>
Tff7b72</template>
Tff7b72</div>
Tff7b72</template>

Tff7b72<scriptTff7b72>
import Utils from "../../js/Utils";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import ContextMenuDivider from "../contextmenu/ContextMenuDivider.vue";
import ContextMenuItem from "../contextmenu/ContextMenuItem.vue";
import ContextMenuPanel from "../contextmenu/ContextMenuPanel.vue";
import ContextMenuSectionLabel from "../contextmenu/ContextMenuSectionLabel.vue";
import DropDownMenu from "../DropDownMenu.vue";
import IconButton from "../IconButton.vue";
import DropDownMenuItem from "../DropDownMenuItem.vue";
import DialogUtils from "../../js/DialogUtils";
import GlobalState from "../../js/GlobalState";
import GlobalEmitter from "../../js/GlobalEmitter";
import ToastUtils from "../../js/ToastUtils";
import DownloadUtils from "../../js/DownloadUtils";
import { isUnknownNodeDisplayName } from "../../js/nomadUnknownNodeName.js";
import {
clearLocalNomadFavouritesLayout,
loadNomadFavouritesLayout,
readLocalNomadFavouritesLayout,
saveNomadFavouritesLayout,
} from "../../js/nomadFavouritesLayoutStore.js";

export default {
name: "NomadNetworkSidebar",
components: {
ContextMenuDivider,
ContextMenuItem,
ContextMenuPanel,
ContextMenuSectionLabel,
DropDownMenuItem,
IconButton,
DropDownMenu,
MaterialDesignIcon,
},
props: {
nodes: {
type: Object,
required: true,
},
favourites: {
type: Array,
required: true,
},
selectedDestinationHash: {
type: String,
default: "",
},
nodesSearchTerm: {
type: String,
default: "",
},
totalNodesCount: {
type: Number,
default: 0,
},
isLoadingMoreNodes: {
type: Boolean,
default: false,
},
isSearchingNodes: {
type: Boolean,
default: false,
},
hasMoreNodes: {
type: Boolean,
default: false,
},
collapsed: {
type: Boolean,
default: false,
},
},
emits: [
"node-click",
"rename-favourite",
"remove-favourite",
"add-favourite",
"nodes-search-changed",
"load-more-nodes",
"toggle-collapse",
"bulk-remove-favourites",
"bulk-add-favourites",
],
data() {
return {
GlobalState,
tab: "favourites",
favouritesSearchTerm: "",
favouritesSelectionMode: false,
announcesSelectionMode: false,
selectedFavouriteHashes: [],
selectedAnnounceHashes: [],
favouriteBulkMoveMenuOpen: false,
defaultSectionId: "default",
sections: [],
sectionOrder: [],
favouritesBySection: {},
favouriteLayoutLoadGen: 0,
draggingFavouriteHash: null,
draggingFavouriteHashes: [],
draggingFavouriteSectionId: null,
dragOverSectionId: null,
draggingSectionId: null,
draggingSectionOverId: null,
favouriteContextMenu: {
show: false,
x: 0,
y: 0,
targetHash: null,
targetSectionId: null,
justOpened: false,
},
sectionContextMenu: {
show: false,
x: 0,
y: 0,
sectionId: null,
},
announceContextMenu: {
show: false,
x: 0,
y: 0,
node: null,
justOpened: false,
},
smUp: typeof window !== "undefined" ? window.innerWidth >= 640 : true,
editingSectionId: null,
editingSectionName: "",
};
},
computed: {
effectiveCollapsed() {
return this.collapsed && this.smUp;
},
sidebarRootClass() {
if (this.effectiveCollapsed) {
return "flex flex-col w-16 min-w-16 max-w-16 h-full min-h-0 bg-white dark:bg-zinc-950 border-r border-gray-200 dark:border-zinc-800";
}
return "flex flex-col w-full sm:w-80 sm:min-w-80 md:max-lg:w-64 md:max-lg:min-w-64 lg:w-80 lg:min-w-80 min-h-0 bg-white dark:bg-zinc-950 border-r border-gray-200 dark:border-zinc-800";
},
blockedDestinations() {
return GlobalState.blockedDestinations;
},
nodesCount() {
return Object.keys(this.nodes).length;
},
nodesOrderedByLatestAnnounce() {
const nodes = Object.values(this.nodes);
return nodes.sort(function (nodeA, nodeB) {
// order by updated_at desc
const nodeAUpdatedAt = new Date(nodeA.updated_at).getTime();
const nodeBUpdatedAt = new Date(nodeB.updated_at).getTime();
return nodeBUpdatedAt - nodeAUpdatedAt;
});
},
searchedNodes() {
return this.nodesOrderedByLatestAnnounce.filter((node) => {
const search = this.nodesSearchTerm.toLowerCase();
const label = (node.custom_display_name || node.display_name || "").toLowerCase();
const matchesDisplayName = label.includes(search);
const matchesDestinationHash = node.destination_hash.toLowerCase().includes(search);
return matchesDisplayName || matchesDestinationHash;
});
},
orderedSections() {
const map = {};
this.sections.forEach((section) => {
map[section.id] = section;
});
const ids = this.sectionOrder.length > 0 ? this.sectionOrder : this.sections.map((section) => section.id);
return ids.map((id) => map[id]).filter((section) => section);
},
sectionsWithFavourites() {
const search = this.favouritesSearchTerm.toLowerCase();
return this.orderedSections.map((section) => {
const hashes = this.favouritesBySection[section.id] || [];
const favourites = hashes
.map((hash) => this.favourites.find((fav) => fav.destination_hash === hash))
.filter((fav) => fav)
.filter((fav) => this.matchesFavouriteSearch(fav, search));
return { ...section, favourites };
});
},
favouritesSearchNoResults() {
if (this.favourites.length === 0) {
return false;
}
if (this.favouritesSearchTerm.trim() === "") {
return false;
}
return !this.sectionsWithFavourites.some((section) => section.favourites.length > 0);
},
collapsedFavouritePreview() {
const out = [];
const max = 5;
for (const section of this.orderedSections) {
const hashes = this.favouritesBySection[section.id] || [];
for (const hash of hashes) {
const fav = this.favourites.find((f) => f.destination_hash === hash);
if (!fav) {
continue;
}
if (out.length >= max) {
return out;
}
out.push(fav);
}
}
return out;
},
collapsedAnnounceNodesPreview() {
return this.nodesOrderedByLatestAnnounce.slice(0, 5);
},
flatVisibleFavouriteDestinationHashes() {
const out = [];
for (const section of this.sectionsWithFavourites) {
for (const fav of section.favourites) {
out.push(fav.destination_hash);
}
}
return out;
},
flatVisibleAnnounceDestinationHashes() {
return this.searchedNodes.map((n) => n.destination_hash);
},
allVisibleFavouritesSelected() {
const ids = this.flatVisibleFavouriteDestinationHashes;
return ids.length > 0 && ids.every((id) => this.selectedFavouriteHashes.includes(id));
},
allVisibleAnnouncesSelected() {
const ids = this.flatVisibleAnnounceDestinationHashes;
return ids.length > 0 && ids.every((id) => this.selectedAnnounceHashes.includes(id));
},
},
watch: {
favourites: {
handler() {
this.ensureFavouriteLayout();
},
deep: true,
},
tab() {
this.exitFavouritesSelectionMode();
this.exitAnnouncesSelectionMode();
this.favouriteBulkMoveMenuOpen = false;
},
},
mounted() {
this._layoutPersistTimer = null;
// Paint immediately from local cache/defaults, then hydrate from the identity DB.
this.applyFavouriteLayout(readLocalNomadFavouritesLayout());
this.ensureFavouriteLayout();
this.reloadFavouriteLayoutFromStore();
this._smUpMql = window.matchMedia("(min-width: 640px)");
this._smUpResize = () => {
this.smUp = this._smUpMql.matches;
};
this._smUpResize();
this._smUpMql.addEventListener("change", this._smUpResize);
this._onNomadnetFavouritesLayoutImported = () => {
this.reloadFavouriteLayoutFromStore();
};
GlobalEmitter.on("nomadnet-favourites-layout-imported", this._onNomadnetFavouritesLayoutImported);
this._onIdentitySwitched = () => {
clearLocalNomadFavouritesLayout();
this.resetDefaultSections();
this.reloadFavouriteLayoutFromStore();
};
GlobalEmitter.on("identity-switched", this._onIdentitySwitched);
},
unmounted() {
if (this._layoutPersistTimer) {
clearTimeout(this._layoutPersistTimer);
this._layoutPersistTimer = null;
this.persistFavouriteLayout({ immediate: true });
}
if (this._smUpMql && this._smUpResize) {
this._smUpMql.removeEventListener("change", this._smUpResize);
}
if (this._onNomadnetFavouritesLayoutImported) {
GlobalEmitter.off("nomadnet-favourites-layout-imported", this._onNomadnetFavouritesLayoutImported);
}
if (this._onIdentitySwitched) {
GlobalEmitter.off("identity-switched", this._onIdentitySwitched);
}
},
methods: {
applyFavouriteLayout(layout) {
if (!layout) {
if (this.sections.length === 0) {
this.resetDefaultSections();
}
return;
}
this.sections = layout.sections || [];
this.sectionOrder =
layout.sectionOrder ||
(layout.sections ? layout.sections.map((section) => section.id) : this.sectionOrder);
this.favouritesBySection = layout.favouritesBySection || {};
if (this.sections.length === 0) {
this.resetDefaultSections();
}
},
async reloadFavouriteLayoutFromStore() {
const gen = ++this.favouriteLayoutLoadGen;
const layout = await loadNomadFavouritesLayout(window.api);
if (gen !== this.favouriteLayoutLoadGen) {
return;
}
this.applyFavouriteLayout(layout);
this.ensureFavouriteLayout();
},
toggleFavouritesSelectionMode() {
this.favouritesSelectionMode = !this.favouritesSelectionMode;
if (!this.favouritesSelectionMode) {
this.selectedFavouriteHashes = [];
}
this.favouriteBulkMoveMenuOpen = false;
},
toggleAnnouncesSelectionMode() {
this.announcesSelectionMode = !this.announcesSelectionMode;
if (!this.announcesSelectionMode) {
this.selectedAnnounceHashes = [];
}
},
exitFavouritesSelectionMode() {
this.favouritesSelectionMode = false;
this.selectedFavouriteHashes = [];
this.favouriteBulkMoveMenuOpen = false;
},
exitAnnouncesSelectionMode() {
this.announcesSelectionMode = false;
this.selectedAnnounceHashes = [];
},
toggleSelectFavourite(hash) {
const i = this.selectedFavouriteHashes.indexOf(hash);
if (i >= 0) {
this.selectedFavouriteHashes.splice(i, 1);
} else {
this.selectedFavouriteHashes.push(hash);
}
},
toggleSelectAnnounce(hash) {
const i = this.selectedAnnounceHashes.indexOf(hash);
if (i >= 0) {
this.selectedAnnounceHashes.splice(i, 1);
} else {
this.selectedAnnounceHashes.push(hash);
}
},
toggleSelectAllVisibleFavourites() {
const ids = this.flatVisibleFavouriteDestinationHashes;
if (ids.length === 0) {
return;
}
if (ids.every((id) => this.selectedFavouriteHashes.includes(id))) {
this.selectedFavouriteHashes = this.selectedFavouriteHashes.filter((h) => !ids.includes(h));
} else {
this.selectedFavouriteHashes = [...new Set([...this.selectedFavouriteHashes, ...ids])];
}
},
toggleSelectAllVisibleAnnounces() {
const ids = this.flatVisibleAnnounceDestinationHashes;
if (ids.length === 0) {
return;
}
if (ids.every((id) => this.selectedAnnounceHashes.includes(id))) {
this.selectedAnnounceHashes = this.selectedAnnounceHashes.filter((h) => !ids.includes(h));
} else {
this.selectedAnnounceHashes = [...new Set([...this.selectedAnnounceHashes, ...ids])];
}
},
onFavouriteRowActivate(favourite) {
if (this.isBlocked(favourite.destination_hash)) {
return;
}
if (this.favouritesSelectionMode) {
this.toggleSelectFavourite(favourite.destination_hash);
return;
}
this.onFavouriteClick(favourite);
},
onAnnounceRowActivate(node) {
if (this.isBlocked(node.identity_hash || node.destination_hash)) {
return;
}
if (this.announcesSelectionMode) {
this.toggleSelectAnnounce(node.destination_hash);
return;
}
this.onNodeClick(node);
},
isFavouriteRowDragging(destinationHash) {
return this.draggingFavouriteHashes.includes(destinationHash);
},
closeFavouriteBulkMoveMenu() {
this.favouriteBulkMoveMenuOpen = false;
},
bulkMoveSelectedFavouritesToSection(sectionId) {
if (!sectionId || this.selectedFavouriteHashes.length === 0) {
this.closeFavouriteBulkMoveMenu();
return;
}
this.moveFavouritesToSection([...this.selectedFavouriteHashes], sectionId);
this.closeFavouriteBulkMoveMenu();
this.exitFavouritesSelectionMode();
},
async bulkRemoveSelectedFavourites() {
const hashes = [...this.selectedFavouriteHashes];
if (hashes.length === 0) {
return;
}
if (
!(await DialogUtils.confirm(
this.$t("nomadnet.bulk_remove_favourites_confirm", { count: hashes.length })
))
) {
return;
}
this.$emit("bulk-remove-favourites", hashes);
this.exitFavouritesSelectionMode();
},
async bulkBanishSelectedAnnounces() {
const nodes = this.selectedAnnounceHashes
.map((h) => this.nodes[h])
.filter((n) => n && !this.isBlocked(n.identity_hash) && !this.isBlocked(n.destination_hash));
if (nodes.length === 0) {
return;
}
if (!(await DialogUtils.confirm(this.$t("nomadnet.bulk_block_confirm", { count: nodes.length })))) {
return;
}
try {
for (const node of nodes) {
await window.api.post("/api/v1/blocked-destinations", {
destination_hash: node.identity_hash,
});
}
GlobalEmitter.emit("block-status-changed");
ToastUtils.success(this.$t("nomadnet.bulk_block_done", { count: nodes.length }));
} catch (e) {
DialogUtils.alert(this.$t("nomadnet.failed_to_block_node"));
console.error(e);
}
this.exitAnnouncesSelectionMode();
},
bulkAddSelectedAnnouncesToFavourites() {
const nodes = this.selectedAnnounceHashes
.map((h) => this.nodes[h])
.filter((n) => n && !this.isFavourite(n.destination_hash));
if (nodes.length === 0) {
ToastUtils.info(this.$t("nomadnet.bulk_nothing_to_add_favourites"));
return;
}
this.$emit("bulk-add-favourites", nodes);
this.exitAnnouncesSelectionMode();
},
startEditingSection(section) {
this.editingSectionId = section.id;
this.editingSectionName = section.name;
this.$nextTick(() => {
const el = this.$refs[`sectionInput-${section.id}`];
if (el && el[0]) el[0].focus();
});
},
saveSectionName() {
if (!this.editingSectionId) return;
const sectionId = this.editingSectionId;
const name = this.editingSectionName.trim();
if (name) {
this.sections = this.sections.map((sec) => (sec.id === sectionId ? { ...sec, name } : sec));
this.persistFavouriteLayout();
}
this.cancelEditingSection();
},
cancelEditingSection() {
this.editingSectionId = null;
this.editingSectionName = "";
},
matchesFavouriteSearch(favourite, searchTerm = this.favouritesSearchTerm.toLowerCase()) {
const matchesDisplayName = favourite.display_name.toLowerCase().includes(searchTerm);
const matchesCustomDisplayName =
favourite.custom_display_name?.toLowerCase()?.includes(searchTerm) === true;
const matchesDestinationHash = favourite.destination_hash.toLowerCase().includes(searchTerm);
return matchesDisplayName || matchesCustomDisplayName || matchesDestinationHash;
},
buildDefaultSection() {
return {
id: this.defaultSectionId,
name: this.$t("nomadnet.favourites"),
collapsed: false,
};
},
resetDefaultSections() {
const defaultSection = this.buildDefaultSection();
this.sections = [defaultSection];
this.sectionOrder = [defaultSection.id];
this.favouritesBySection = { [defaultSection.id]: [] };
},
loadFavouriteLayout() {
void this.reloadFavouriteLayoutFromStore();
},
persistFavouriteLayout(options = {}) {
// User-driven saves invalidate in-flight remote loads so they cannot clobber edits.
// Reconciliation persists (fromEnsure) must not, or the first hydrate is discarded.
if (!options.fromEnsure) {
this.favouriteLayoutLoadGen += 1;
}
const layout = {
sections: this.sections,
sectionOrder: this.sectionOrder,
favouritesBySection: this.favouritesBySection,
};
const flush = () => {
this._layoutPersistTimer = null;
if (typeof window === "undefined" || !window.api) {
return undefined;
}
return saveNomadFavouritesLayout(window.api, layout);
};
if (options.immediate) {
if (this._layoutPersistTimer) {
clearTimeout(this._layoutPersistTimer);
this._layoutPersistTimer = null;
}
return flush();
}
if (this._layoutPersistTimer) {
clearTimeout(this._layoutPersistTimer);
}
this._layoutPersistTimer = setTimeout(() => {
void flush();
}, 250);
return undefined;
},
ensureFavouriteLayout() {
if (!Array.isArray(this.favourites) || this.favourites.length === 0) {
return;
}
if (this.sections.length === 0) {
this.resetDefaultSections();
}
const hashes = this.favourites.map((fav) => fav.destination_hash);
const sectionIds = new Set();
const sanitizedSections = [];
this.sections.forEach((section) => {
if (!section || !section.id || sectionIds.has(section.id)) {
return;
}
sectionIds.add(section.id);
sanitizedSections.push({
id: section.id,
name: section.name || this.$t("nomadnet.favourites"),
collapsed: section.collapsed === true ? true : false,
});
});
if (!sectionIds.has(this.defaultSectionId)) {
const defaultSection = this.buildDefaultSection();
sanitizedSections.unshift(defaultSection);
sectionIds.add(defaultSection.id);
}
const existingOrder = Array.isArray(this.sectionOrder) ? this.sectionOrder : [];
const filteredOrder = existingOrder.filter((id) => sectionIds.has(id));
const remaining = sanitizedSections
.map((section) => section.id)
.filter((id) => !filteredOrder.includes(id));
const nextSectionOrder = [...filteredOrder, ...remaining];

const nextFavouritesBySection = {};
sanitizedSections.forEach((section) => {
const existing = this.favouritesBySection[section.id] || [];
nextFavouritesBySection[section.id] = existing.filter((hash) => hashes.includes(hash));
});
const assigned = new Set(Object.values(nextFavouritesBySection).flat());
hashes.forEach((hash) => {
if (!assigned.has(hash)) {
nextFavouritesBySection[this.defaultSectionId].push(hash);
assigned.add(hash);
}
});

const sectionsChanged = JSON.stringify(this.sections) !== JSON.stringify(sanitizedSections);
const orderChanged = JSON.stringify(this.sectionOrder) !== JSON.stringify(nextSectionOrder);
const favouritesChanged =
JSON.stringify(this.favouritesBySection) !== JSON.stringify(nextFavouritesBySection);
this.sections = sanitizedSections;
this.sectionOrder = nextSectionOrder;
this.favouritesBySection = nextFavouritesBySection;
if (sectionsChanged || orderChanged || favouritesChanged) {
this.persistFavouriteLayout({ fromEnsure: true });
}
},
isBlocked(identityHash) {
return this.blockedDestinations.some((b) => b.destination_hash === identityHash);
},
isFavourite(destinationHash) {
return this.favourites.some((f) => f.destination_hash === destinationHash);
},
favouriteDisplayName(favourite) {
if (!favourite) {
return "";
}
const hash = favourite.destination_hash;
const cached = hash ? this.nodes?.[hash] : null;
const cachedName = cached?.custom_display_name || cached?.display_name || "";
if (cachedName && !isUnknownNodeDisplayName(cachedName, this.$t("nomadnet.unknown_node"))) {
return cachedName;
}
const favouriteName = favourite.custom_display_name || favourite.display_name || "";
if (favouriteName && !isUnknownNodeDisplayName(favouriteName, this.$t("nomadnet.unknown_node"))) {
return favouriteName;
}
return favouriteName || this.$t("nomadnet.unknown_node");
},
addFavouriteFromContext() {
const node = this.announceContextMenu.node;
if (!node) {
this.closeContextMenus();
return;
}
this.closeContextMenus();
this.$emit("add-favourite", node);
},
async blockAnnounceFromContext() {
const node = this.announceContextMenu.node;
if (!node) {
this.closeContextMenus();
return;
}
this.closeContextMenus();
await this.onBlockNode(node);
},
async unblockAnnounceFromContext() {
const node = this.announceContextMenu.node;
if (!node) {
this.closeContextMenus();
return;
}
this.closeContextMenus();
await this.onUnblockNode(node.identity_hash);
},
async onBlockNode(node) {
if (!(await DialogUtils.confirm(this.$t("nomadnet.block_node_confirm", { name: node.display_name })))) {
return;
}

try {
await window.api.post("/api/v1/blocked-destinations", {
destination_hash: node.identity_hash,
});
GlobalEmitter.emit("block-status-changed");
DialogUtils.alert(this.$t("nomadnet.node_blocked_successfully"));
} catch (e) {
DialogUtils.alert(this.$t("nomadnet.failed_to_block_node"));
console.log(e);
}
},
async onUnblockNode(identityHash) {
try {
await window.api.delete(`/api/v1/blocked-destinations/${identityHash}`);
GlobalEmitter.emit("block-status-changed");
DialogUtils.alert(this.$t("nomadnet.banishment_lifted"));
} catch (e) {
DialogUtils.alert(this.$t("nomadnet.failed_lift_banishment"));
console.log(e);
}
},
onNodeClick(node) {
if (this.isBlocked(node.identity_hash || node.destination_hash)) {
return;
}
this.$emit("node-click", node);
},
onFavouriteClick(favourite) {
if (this.isBlocked(favourite.destination_hash)) {
return;
}
this.onNodeClick(favourite);
},
onRenameFavourite(favourite) {
this.$emit("rename-favourite", favourite);
},
onRemoveFavourite(favourite) {
this.$emit("remove-favourite", favourite);
},
onFavouriteDragStart(event, favourite, sectionId) {
let hashes;
if (
this.favouritesSelectionMode &&
this.selectedFavouriteHashes.includes(favourite.destination_hash) &&
this.selectedFavouriteHashes.length > 1
) {
hashes = [...this.selectedFavouriteHashes];
} else {
hashes = [favourite.destination_hash];
}
try {
if (event?.dataTransfer) {
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", favourite.destination_hash);
if (hashes.length > 1) {
event.dataTransfer.setData("application/x-meshchat-nomad-favs", JSON.stringify(hashes));
}
}
} catch {
// ignore for browsers that prevent setting drag meta
}
this.draggingFavouriteHashes = hashes;
this.draggingFavouriteHash = favourite.destination_hash;
this.draggingFavouriteSectionId = sectionId;
},
onFavouriteDragOver(event) {
if (event?.dataTransfer) {
event.dataTransfer.dropEffect = "move";
}
},
onFavouriteDrop(event, targetSectionId, targetFavourite) {
const hashes =
this.draggingFavouriteHashes.length > 0
? [...this.draggingFavouriteHashes]
: this.draggingFavouriteHash
? [this.draggingFavouriteHash]
: [];
if (!hashes.length) {
return;
}
if (hashes.includes(targetFavourite.destination_hash)) {
return;
}
this.moveFavouritesToSection(hashes, targetSectionId, targetFavourite.destination_hash);
},
onFavouriteDragEnd() {
this.draggingFavouriteHash = null;
this.draggingFavouriteHashes = [];
this.draggingFavouriteSectionId = null;
this.dragOverSectionId = null;
},
onSectionDragOver(sectionId) {
this.dragOverSectionId = sectionId;
},
onSectionDragLeave() {
this.dragOverSectionId = null;
},
onDropOnSection(sectionId) {
const hashes =
this.draggingFavouriteHashes.length > 0
? [...this.draggingFavouriteHashes]
: this.draggingFavouriteHash
? [this.draggingFavouriteHash]
: [];
if (!hashes.length) {
return;
}
this.moveFavouritesToSection(hashes, sectionId);
},
onSectionDragStart(sectionId) {
this.draggingSectionId = sectionId;
},
onSectionReorderDragOver(sectionId) {
if (!this.draggingSectionId || this.draggingSectionId === sectionId) {
return;
}
this.draggingSectionOverId = sectionId;
},
onSectionDrop(targetSectionId) {
if (!this.draggingSectionId || this.draggingSectionId === targetSectionId) {
this.onSectionDragEnd();
return;
}
const currentOrder = [...this.sectionOrder];
const fromIndex = currentOrder.indexOf(this.draggingSectionId);
const toIndex = currentOrder.indexOf(targetSectionId);
if (fromIndex === -1 || toIndex === -1) {
this.onSectionDragEnd();
return;
}
currentOrder.splice(fromIndex, 1);
currentOrder.splice(toIndex, 0, this.draggingSectionId);
this.sectionOrder = currentOrder;
this.persistFavouriteLayout();
this.onSectionDragEnd();
},
onSectionDragEnd() {
this.draggingSectionId = null;
this.draggingSectionOverId = null;
},
moveFavouriteToSection(hash, targetSectionId, beforeHash = null) {
this.moveFavouritesToSection([hash], targetSectionId, beforeHash);
},
moveFavouritesToSection(hashes, targetSectionId, beforeHash = null) {
const unique = [...new Set((hashes || []).filter(Boolean))];
if (!unique.length || !targetSectionId) {
return;
}
const updated = {};
Object.keys(this.favouritesBySection).forEach((sectionKey) => {
updated[sectionKey] = [...(this.favouritesBySection[sectionKey] || [])].filter(
(value) => !unique.includes(value)
);
});

if (!updated[targetSectionId]) {
updated[targetSectionId] = [];
}

let targetList = [...updated[targetSectionId]];

if (beforeHash && !unique.includes(beforeHash)) {
const insertIndex = targetList.indexOf(beforeHash);
if (insertIndex === -1) {
targetList.push(...unique);
} else {
targetList.splice(insertIndex, 0, ...unique);
}
} else {
targetList.push(...unique);
}

updated[targetSectionId] = targetList;
this.favouritesBySection = updated;
this.persistFavouriteLayout();
this.draggingFavouriteHash = null;
this.draggingFavouriteHashes = [];
this.draggingFavouriteSectionId = null;
this.dragOverSectionId = null;
},
openFavouriteContextMenu(event, favourite, sectionId) {
this.favouriteContextMenu = {
show: true,
justOpened: true,
x: event.clientX,
y: event.clientY,
targetHash: favourite.destination_hash,
targetSectionId: sectionId,
};
this.sectionContextMenu.show = false;
setTimeout(() => {
this.favouriteContextMenu.justOpened = false;
}, 50);
},
openSectionContextMenu(event, section) {
this.sectionContextMenu = {
show: true,
x: event.pageX || event.clientX,
y: event.pageY || event.clientY,
sectionId: section.id,
};
this.favouriteContextMenu.show = false;
},
async exportSectionFavouritesFromContext() {
const sid = this.sectionContextMenu.sectionId;
if (!sid) {
this.closeContextMenus();
return;
}
const section = this.sections.find((s) => s.id === sid);
this.closeContextMenus();
if (!section) {
return;
}
const hashes = this.favouritesBySection[sid] || [];
const payload = {
format: "meshchatx/nomadnet_favourites_section/v1",
exported_at: new Date().toISOString(),
section: {
id: section.id,
name: section.name,
collapsed: section.collapsed === true,
},
destination_hashes: hashes.filter((h) => typeof h === "string"),
};
const slug = (section.name || "section")
.replace(/[^a-z0-9]+/gi, "_")
.replace(/^_|_$/g, "")
.slice(0, 48);
const namePart = slug || "section";
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
try {
await DownloadUtils.downloadFile(`nomadnet_favourites_section_${namePart}.json`, blob);
ToastUtils.success(this.$t("nomadnet.section_favourites_exported"));
} catch {
ToastUtils.error(this.$t("nomadnet.section_favourites_export_failed"));
}
},
closeContextMenus() {
this.favouriteContextMenu.show = false;
this.sectionContextMenu.show = false;
this.announceContextMenu.show = false;
this.favouriteBulkMoveMenuOpen = false;
},
openAnnounceContextMenu(event, node) {
this.announceContextMenu = {
show: true,
justOpened: true,
x: event.clientX,
y: event.clientY,
node,
};
this.favouriteContextMenu.show = false;
this.sectionContextMenu.show = false;
setTimeout(() => {
this.announceContextMenu.justOpened = false;
}, 50);
},
getFavouriteByHash(hash) {
return this.favourites.find((fav) => fav.destination_hash === hash);
},
renameFavouriteFromContext() {
const favourite = this.getFavouriteByHash(this.favouriteContextMenu.targetHash);
if (!favourite) {
this.closeContextMenus();
return;
}
this.closeContextMenus();
this.onRenameFavourite(favourite);
},
removeFavouriteFromContext() {
const favourite = this.getFavouriteByHash(this.favouriteContextMenu.targetHash);
if (!favourite) {
this.closeContextMenus();
return;
}
this.closeContextMenus();
this.onRemoveFavourite(favourite);
},
async banishFavouriteFromContext() {
const favourite = this.getFavouriteByHash(this.favouriteContextMenu.targetHash);
if (!favourite) {
this.closeContextMenus();
return;
}
this.closeContextMenus();
if (
!(await DialogUtils.confirm(
this.$t("nomadnet.block_node_confirm", {
name: favourite.display_name || favourite.custom_display_name,
})
))
) {
return;
}
try {
await window.api.post("/api/v1/blocked-destinations", {
destination_hash: favourite.destination_hash,
});
GlobalEmitter.emit("block-status-changed");
DialogUtils.alert(this.$t("nomadnet.node_blocked_successfully"));
} catch (e) {
DialogUtils.alert(this.$t("nomadnet.failed_to_block_node"));
console.error(e);
}
},
async unblockFavouriteFromContext() {
const hash = this.favouriteContextMenu.targetHash;
if (!hash) {
this.closeContextMenus();
return;
}
this.closeContextMenus();
try {
await window.api.delete(`/api/v1/blocked-destinations/${hash}`);
GlobalEmitter.emit("block-status-changed");
DialogUtils.alert(this.$t("nomadnet.banishment_lifted"));
} catch (e) {
DialogUtils.alert(this.$t("nomadnet.failed_lift_banishment"));
console.error(e);
}
},
moveContextFavouriteToSection(sectionId) {
if (!this.favouriteContextMenu.targetHash) {
return;
}
this.moveFavouriteToSection(this.favouriteContextMenu.targetHash, sectionId);
this.closeContextMenus();
},
toggleSectionCollapse(sectionId) {
const idx = this.sections.findIndex((section) => section.id === sectionId);
if (idx === -1) {
return;
}
const updated = [...this.sections];
const section = { ...updated[idx] };
section.collapsed = !section.collapsed;
updated[idx] = section;
this.sections = updated;
this.persistFavouriteLayout();
},
async createSection() {
const name = await DialogUtils.prompt(
this.$t("nomadnet.enter_section_name"),
this.$t("nomadnet.new_section")
);
if (!name) {
return;
}
const section = {
id: `section-${Date.now()}`,
name,
collapsed: false,
};
this.sections = [...this.sections, section];
this.sectionOrder = [...this.sectionOrder, section.id];
this.favouritesBySection = { ...this.favouritesBySection, [section.id]: [] };
this.persistFavouriteLayout();
},
async renameSectionFromContext() {
const section = this.sections.find((sec) => sec.id === this.sectionContextMenu.sectionId);
if (!section) {
this.closeContextMenus();
return;
}
const name = await DialogUtils.prompt(this.$t("nomadnet.rename_section"), section.name);
if (!name || name === section.name) {
this.closeContextMenus();
return;
}
this.sections = this.sections.map((sec) => (sec.id === section.id ? { ...sec, name } : sec));
this.persistFavouriteLayout();
this.closeContextMenus();
},
async removeSectionFromContext() {
const sectionId = this.sectionContextMenu.sectionId;
if (!sectionId || sectionId === this.defaultSectionId) {
this.closeContextMenus();
return;
}
const confirmed = await DialogUtils.confirm(this.$t("nomadnet.delete_section_confirm"));
if (!confirmed) {
this.closeContextMenus();
return;
}
const retainedSections = this.sections.filter((section) => section.id !== sectionId);
const migrated = this.favouritesBySection[sectionId] || [];
const nextMap = { ...this.favouritesBySection };
delete nextMap[sectionId];
nextMap[this.defaultSectionId] = [...(nextMap[this.defaultSectionId] || []), ...migrated];
this.sections = retainedSections;
this.sectionOrder = this.sectionOrder.filter((id) => id !== sectionId);
this.favouritesBySection = nextMap;
this.persistFavouriteLayout();
this.closeContextMenus();
},
formatTimeAgo: function (datetimeString) {
return Utils.formatTimeAgo(datetimeString);
},
formatTimeAgoForI18n: function (datetimeString) {
return Utils.formatTimeAgoForI18n(datetimeString);
},
formatDestinationHash: function (destinationHash) {
return Utils.formatDestinationHash(destinationHash);
},
copyToClipboard(text, label) {
if (!text) return;
navigator.clipboard.writeText(text);
ToastUtils.success(`${label} copied to clipboard`);
},
onNodesSearchInput(event) {
this.$emit("nodes-search-changed", event.target.value);
},
onNodesScroll(event) {
const element = event.target;
// if scrolled near bottom (within 200px)
if (element.scrollHeight - element.scrollTop - element.clientHeight Tff7b72< 200) {
if (this.hasMoreNodes && !this.isLoadingMoreNodes) {
this.$emit("load-more-nodes");
}
}
},
},
};
</scriptTff7b72>

Tff7b72<style scopedTff7b72>
@reference "../../style.css";
.sidebar-tab {
@apply flex h-full w-1/2 items-center justify-center text-sm font-semibold text-gray-500 dark:text-gray-400 border-b-2 border-transparent transition;
}
.sidebar-tab--active {
@apply text-blue-600 border-blue-500 dark:text-blue-300 dark:border-blue-400;
}
.favourite-card {
@apply flex items-center gap-3 rounded-2xl border border-gray-200 dark:border-zinc-800 bg-white/90 dark:bg-zinc-900/70 px-3 py-2 cursor-pointer hover:border-blue-400 dark:hover:border-blue-500 hover:z-10;
}
.favourite-card--active {
@apply border-blue-500 dark:border-blue-400 bg-blue-50/60 dark:bg-blue-900/30;
}
.favourite-card__icon,
.announce-card__icon {
@apply w-10 h-10 rounded-xl bg-gray-100 dark:bg-zinc-800 flex items-center justify-center text-gray-500 dark:text-gray-300;
}
.favourite-card--dragging {
@apply opacity-60 ring-2 ring-blue-300 dark:ring-blue-600;
}
.announce-card {
@apply flex items-center gap-3 rounded-2xl border border-gray-200 dark:border-zinc-800 bg-white/90 dark:bg-zinc-900/70 px-3 py-2 hover:border-blue-400 dark:hover:border-blue-500 hover:z-10;
}
.announce-card--active {
@apply border-blue-500 dark:border-blue-400 bg-blue-50/70 dark:bg-blue-900/30;
}
.empty-state {
@apply flex flex-col items-center justify-center text-center gap-2 text-gray-500 dark:text-gray-400;
}
.empty-state--compact {
@apply justify-center py-3;
}
.empty-state--panel {
@apply min-h-[min(50vh,18rem)] py-10 sm:min-h-[min(45vh,20rem)];
}
Tff7b72</style>

Served by rngit 1.5.2 - Generated in 0.09s